package com.tianji.promotion.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.tianji.api.cache.CategoryCache;
import com.tianji.common.domain.dto.PageDTO;
import com.tianji.common.exceptions.BadRequestException;
import com.tianji.common.exceptions.BizIllegalException;
import com.tianji.common.utils.*;
import com.tianji.promotion.constants.PromotionConstants;
import com.tianji.promotion.domain.dto.CouponFormDTO;
import com.tianji.promotion.domain.dto.CouponIssueFormDTO;
import com.tianji.promotion.domain.po.Coupon;
import com.tianji.promotion.domain.po.CouponScope;
import com.tianji.promotion.domain.po.UserCoupon;
import com.tianji.promotion.domain.query.CouponQuery;
import com.tianji.promotion.domain.vo.CouponDetailVO;
import com.tianji.promotion.domain.vo.CouponPageVO;
import com.tianji.promotion.domain.vo.CouponScopeVO;
import com.tianji.promotion.domain.vo.CouponVO;
import com.tianji.promotion.enums.CouponStatus;
import com.tianji.promotion.enums.ObtainType;
import com.tianji.promotion.enums.UserCouponStatus;
import com.tianji.promotion.mapper.CouponMapper;
import com.tianji.promotion.service.CouponScopeService;
import com.tianji.promotion.service.CouponService;
import com.tianji.promotion.service.ExchangeCodeService;
import com.tianji.promotion.service.UserCouponService;
import lombok.RequiredArgsConstructor;
import org.checkerframework.checker.units.qual.N;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import static com.tianji.promotion.enums.CouponStatus.*;

/**
 * @author Cammy
 * @description 针对表【coupon(优惠券的规则信息)】的数据库操作Service实现
 * @createDate 2025-02-25 05:58:13
 */
@Service
@RequiredArgsConstructor
public class CouponServiceImpl extends ServiceImpl<CouponMapper, Coupon>
        implements CouponService {

    private final CouponScopeService couponScopeService;

    private final ExchangeCodeService exchangeCodeService;

    private final CategoryCache categoryCache;

    private final UserCouponService userCouponService;

    /**
     * TODO:新增优惠券接口
     * @param couponFormDTO
     */
    @Override
    @Transactional
    public void saveCoupon(CouponFormDTO couponFormDTO) {
        // 1.保存优惠券
        // 1.1 转PO
        Coupon coupon = BeanUtils.toBean(couponFormDTO, Coupon.class);
        // 1.2 保存
        this.save(coupon);

        if (!couponFormDTO.getSpecific()) {
            // 没有限定范围
            return;
        }
        Long couponId = coupon.getId();
        // 2.保存限定范围
        List<Long> scopes = couponFormDTO.getScopes();
        if (CollUtils.isEmpty(scopes)) {
            throw new BadRequestException("限定范围不能为空");
        }
        // 2.1 转PO
        List<CouponScope> list = scopes.stream()
                .map(bizId -> new CouponScope().setBizId(bizId).setCouponId(couponId))
                .collect(Collectors.toList());
        // 2.2 保存
        couponScopeService.saveBatch(list);
    }

    /**
     * TODO:分页查询优惠券接口
     *
     * @param couponQuery 用于分页查询优惠券的查询对象，包含了查询所需的条件，如优惠券状态、名称、类型等
     * @return 分页查询结果，封装在 PageDTO 中，包含了符合条件的优惠券分页信息
     */
    @Override
    public PageDTO<CouponPageVO> queryCouponByPage(CouponQuery couponQuery) {
        // 从查询对象中获取优惠券状态
        Integer status = couponQuery.getStatus();
        // 从查询对象中获取优惠券名称
        String name = couponQuery.getName();
        // 从查询对象中获取优惠券类型
        Integer type = couponQuery.getType();
        // 构建分页查询条件
        Page<Coupon> page = lambdaQuery()
                // 若优惠券类型不为空，则添加类型相等的查询条件
                .eq(ObjectUtils.isNotEmpty(type), Coupon::getDiscountType, type)
                // 若优惠券状态不为空，则添加状态相等的查询条件
                .eq(ObjectUtils.isNotEmpty(status), Coupon::getStatus, status)
                // 若优惠券名称不为空，则添加名称模糊匹配的查询条件
                .like(ObjectUtils.isNotEmpty(name), Coupon::getName, name)
                // 执行分页查询，使用查询对象中默认按创建时间降序的分页设置
                .page(couponQuery.toMpPageDefaultSortByCreateTimeDesc());
        // 获取查询结果中的记录列表
        List<Coupon> records = page.getRecords();
        // 如果记录列表为空，返回一个空的分页结果
        if (CollUtils.isEmpty(records)) {
            return PageDTO.empty(page);
        }
        // 将查询结果中的优惠券实体列表转换为优惠券视图对象列表
        List<CouponPageVO> list = BeanUtils.copyList(records, CouponPageVO.class);
        // 将分页信息和视图对象列表封装到 PageDTO 中并返回
        return PageDTO.of(page, list);
    }

    private final StringRedisTemplate redisTemplate;

    /**
     *  TODO:发放优惠券接口
     * @param couponIssueFormDTO 包含优惠券发放所需信息的表单数据传输对象
     */
    @Override
    @Transactional
    public void beginIssue(CouponIssueFormDTO couponIssueFormDTO) {
        // 1.查询优惠券
        // 根据传入的优惠券表单 DTO 中的 ID，从数据库中查询对应的优惠券信息
        Coupon coupon = getById(couponIssueFormDTO.getId());
        // 若查询到的优惠券为空，则抛出 BadRequestException 异常
        if (coupon == null) {
            throw new BadRequestException("优惠券不存在！");
        }
        // 2.判断优惠券状态，是否是暂停或待发放
        // 检查优惠券的状态是否既不是草稿状态也不是暂停状态
        if (coupon.getStatus() != CouponStatus.DRAFT && coupon.getStatus() != PAUSE) {
            // 若状态不符合要求，则抛出 BizIllegalException 异常
            throw new BizIllegalException("优惠券状态错误！");
        }
        // 3.判断是否是立刻发放
        // 从优惠券表单 DTO 中获取优惠券的开始发放时间
        LocalDateTime issueBeginTime = couponIssueFormDTO.getIssueBeginTime();
        // 获取当前的时间
        LocalDateTime now = LocalDateTime.now();
        // 判断是否立刻发放：如果开始发放时间为空或者开始发放时间不晚于当前时间，则立刻发放
        boolean isBegin = issueBeginTime == null || !issueBeginTime.isAfter(now);
        // 4.更新优惠券
        // 4.1.拷贝属性到PO
        // 将优惠券表单 DTO 中的属性复制到新的 Coupon 对象中
        Coupon c = BeanUtils.copyBean(couponIssueFormDTO, Coupon.class);
        // 4.2.更新状态
        // 根据是否立刻发放的判断结果，更新优惠券的状态和开始发放时间
        if (isBegin) {
            // 若立刻发放，将优惠券状态设置为发放中
            c.setStatus(ISSUING);
            // 并将开始发放时间设置为当前时间
            c.setIssueBeginTime(now);
        } else {
            // 若不是立刻发放，将优惠券状态设置为未发放
            c.setStatus(UN_ISSUE);
        }
        // 4.3.写入数据库
        // 将更新后的优惠券信息保存到数据库中
        updateById(c);

        // 5.添加缓存，前缀是立刻发放
        if (isBegin) {
            coupon.setIssueBeginTime(c.getIssueBeginTime());
            coupon.setIssueEndTime(c.getIssueEndTime());
            cacheCouponInfo(coupon);
        }

        // 6.判断是否需要生成兑换码，优惠券类型必须是兑换码，优惠券状态必须是待发放
        // 检查优惠券的获取方式是否为发放兑换码方式，并且优惠券状态是否为草稿状态
        if (coupon.getObtainWay() == ObtainType.ISSUE && coupon.getStatus() == DRAFT) {
            // 将更新后的结束发放时间赋值给原优惠券对象
            coupon.setIssueEndTime(c.getIssueEndTime());
            // 调用兑换码服务的异步生成兑换码方法
            exchangeCodeService.asyncGenerateCode(coupon);
        }
    }

    /**
     * 将优惠券的相关信息缓存到 Redis 中。
     *
     * @param coupon 要缓存信息的优惠券对象
     */
    private void cacheCouponInfo(Coupon coupon) {
        // 1.组织数据
        // 创建一个 HashMap 用于存储要缓存的优惠券信息，初始容量设置为 4，以提高性能
        HashMap<Object, Object> map = new HashMap<>(4);
        // 将优惠券的开始发放时间转换为毫秒数，并以字符串形式存储到 map 中，键为 "issueBeginTime"
        map.put("issueBeginTime", String.valueOf(DateUtils.toEpochMilli(coupon.getIssueBeginTime())));
        // Bug 修复：此处应使用 coupon.getIssueEndTime() 而不是 coupon.getIssueBeginTime()
        // 将优惠券的结束发放时间转换为毫秒数，并以字符串形式存储到 map 中，键为 "issueEndTime"
        map.put("issueEndTime", String.valueOf(DateUtils.toEpochMilli(coupon.getIssueEndTime())));
        // 将优惠券的总数量以字符串形式存储到 map 中，键为 "totalNum"
        map.put("totalNum", String.valueOf(coupon.getTotalNum()));
        // 将优惠券的每人限领数量以字符串形式存储到 map 中，键为 "userLimit"
        map.put("userLimit", String.valueOf(coupon.getUserLimit()));
        // 2.写缓存
        // 使用 Redis 的 Hash 数据结构，将 map 中的所有键值对存储到 Redis 中，
        // 缓存键为促销常量中定义的优惠券缓存键前缀加上优惠券的 ID
        redisTemplate.opsForHash().putAll(PromotionConstants.COUPON_CACHE_KEY_PREFIX + coupon.getId(), map);
    }


    /**
     * 删除优惠券
     * @param id
     */
    @Override
    public void deleteById(Long id) {
        // 1.查询
        Coupon coupon = this.getById(id);
        if (null == coupon || coupon.getStatus() != DRAFT) {
            throw new BadRequestException("优惠券不存在或者优惠券正在使用中");
        }
        // 2.删除优惠券
        boolean success = this.remove(new LambdaQueryWrapper<Coupon>()
                .eq(Coupon::getId, id)
                .eq(Coupon::getStatus, DRAFT));
        if (!success) {
            throw new BadRequestException("优惠券不存在或者优惠券正在使用中");
        }
        // 3.删除优惠券对应限定范围
        if (!coupon.getSpecific()) {
            return;
        }
        couponScopeService.remove(
                new LambdaQueryWrapper<CouponScope>()
                        .eq(CouponScope::getId, id)
        );
    }

    /**
     * TODO:根据id查询优惠券接口
     *
     * @param id 优惠券的ID
     * @return 优惠券详情视图对象
     */
    @Override
    public CouponDetailVO queryCouponById(Long id) {
        // 1.查询优惠券
        // 根据传入的优惠券ID，从数据库中查询对应的优惠券信息
        Coupon coupon = this.getById(id);
        // 2.转为vo
        // 将查询到的优惠券实体对象转换为优惠券详情视图对象
        CouponDetailVO couponDetailVO = BeanUtils.copyBean(coupon, CouponDetailVO.class);
        // 如果转换后的视图对象为空，或者优惠券没有限定范围，则直接返回该视图对象
        if (couponDetailVO == null || !coupon.getSpecific()) {
            // 数据不存在，或者没有限定范围，直接结束
            return couponDetailVO;
        }
        // 3.查询限定范围
        // 通过优惠券范围服务，根据优惠券ID查询对应的优惠券范围列表
        List<CouponScope> list = couponScopeService.lambdaQuery().eq(CouponScope::getCouponId, id).list();
        // 如果查询到的优惠券范围列表为空，则直接返回优惠券详情视图对象
        if (CollUtils.isEmpty(list)) {
            return couponDetailVO;
        }
        // 将查询到的优惠券范围列表转换为优惠券范围视图对象列表
        List<CouponScopeVO> scopeVOList = list.stream()
                // 从每个 CouponScope 对象中提取出业务ID
                .map(CouponScope::getBizId)
                // 根据业务ID创建对应的 CouponScopeVO 对象，同时通过 categoryCache 获取对应的分类名称
                .map(cateId -> new CouponScopeVO(cateId, categoryCache.getNameByLv3Id(cateId)))
                // 将流中的元素收集到一个列表中
                .collect(Collectors.toList());
        // 将转换后的优惠券范围视图对象列表设置到优惠券详情视图对象中
        couponDetailVO.setScopes(scopeVOList);

        return couponDetailVO;
    }

    /**
     * TODO:查询发放中的优惠券
     * @return
     */
    @Override
    public List<CouponVO> queryIssuingCoupons() {
        // 1.查询发放中的优惠券列表
        List<Coupon> coupons = lambdaQuery()
                .eq(Coupon::getStatus, ISSUING)
                .eq(Coupon::getObtainWay, ObtainType.PUBLIC)
                .list();
        if (CollUtils.isEmpty(coupons)) {
            return CollUtils.emptyList();
        }
        // 2.统计当前用户已经领取的优惠券的信息
        List<Long> couponIds = coupons.stream()
                .map(Coupon::getId)
                .collect(Collectors.toList());
        // 2.1.查询当前用户已经领取的优惠券的数据
        List<UserCoupon> userCoupons = userCouponService.lambdaQuery()
                .eq(UserCoupon::getUserId, UserContext.getUser())
                .in(UserCoupon::getCouponId, couponIds)
                .list();
        // 2.2.统计当前用户对优惠券的已经领取数量
        Map<Long, Long> issuedMap = userCoupons.stream()
                .collect(Collectors.groupingBy(UserCoupon::getCouponId, Collectors.counting()));
        // 2.3.统计当前用户对优惠券的已经领取并且未使用的数量
        Map<Long, Long> unusedMap = userCoupons.stream()
                .filter(uc -> uc.getStatus() == UserCouponStatus.UNUSED)
                .collect(Collectors.groupingBy(UserCoupon::getCouponId, Collectors.counting()));
        // 3.封装vo
        List<CouponVO> list = new ArrayList<>(coupons.size());
        for (Coupon coupon : coupons) {
            // 3.1.拷贝PO属性到VO
            CouponVO couponVO = BeanUtils.copyBean(coupon, CouponVO.class);
            list.add(couponVO);
            // 3.2.是否可以领取：已经被领取的数量 < 优惠券总数量 && 当前用户已经领取的数量 < 每人限领数量
            couponVO.setAvailable(
                    coupon.getIssueNum() < coupon.getTotalNum()
                            && issuedMap.getOrDefault(coupon.getId(), 0L) < coupon.getUserLimit()
            );
            // 3.3.是否可以使用：当前用户已经领取并且未使用的优惠券数量 > 0
            couponVO.setReceived(unusedMap.getOrDefault(coupon.getId(), 0L) > 0);
        }
        return list;
    }

    /**
     * 移除缓存
     * @param id
     */
    @Override
    @Transactional
    public void pauseIssue(Long id) {
        // 1.查询旧优惠券
        Coupon coupon = this.getById(id);
        if (null == coupon) {
            throw new BadRequestException("优惠券不存在");
        }

        // 2.当前状态必须是未开始或进行中
        CouponStatus couponStatus = coupon.getStatus();
        if (couponStatus != UN_ISSUE && couponStatus != ISSUING) {
            return;
        }

        // 3.更新状态
        boolean success = lambdaUpdate()
                .set(Coupon::getStatus, PAUSE)
                .eq(Coupon::getId, id)
                .in(Coupon::getStatus, UN_ISSUE, ISSUING)
                .update();
        if (!success) {
            // 可能是重复更新，结束
            log.error("重复暂停优惠券");
        }

        // 4.删除缓存
        redisTemplate.delete(PromotionConstants.COUPON_CACHE_KEY_PREFIX + id);
    }

}




